CQRS and Mediator Design Patterns in .Net 6

Introduction

CQRS

CQRS stands for Command and Query Responsibility Segregation, a design pattern that separates read and update operations for a data store. Implementing CQRS in your application can maximize its performance, scalability, and security. The flexibility created by migrating to CQRS allows a system to better evolve over time and prevents update commands from causing merge conflicts at the domain level. I’ve posted an article explaining how a CQRS pattern can be used to scale the MySQL database horizontally

Mediator

Mediator design pattern is one of the important and widely used behavioral design patterns. Mediator enables decoupling of objects by introducing a layer in between so that the interaction between objects happens via the layer. If the objects interact with each other directly, the system components are tightly-coupled with each other that makes higher maintainability cost and not hard to extend. Mediator pattern focuses on providing a mediator between objects for communication and help in implementing loose-coupling between objects.

Problem

In traditional architecture, we use the same database for query and update operations. That is simple and works well for basic CRUD operations. In more complex applications, however, this approach can become unmanageable. Then you start refactoring your code and try to separate read and update calls probably by implementing the CQRS pattern. In the DotNet world developers use one service to manage everything related to one specific entity, but if you implement CQRS pattern then you need multiple services for queries and commands and if you inject all these dependencies using Dependency Injection, then there will be lot of services in a simple Controller.

Solution

Mediator pattern can help you resolve the above problem. One mediator class or library can call the required commands and query services based on the input models. So you just need to inject one Interface and that interface will manage further dependencies. We use Dependency Injection to make our application loosely coupled, but the Mediator pattern will make this further de-coupled and simplified.

Package

The MediatR Nuget package can be used to implement the Mediator pattern in .Net. You can use the below commands to install the required packages

    dotnet add package MediatR    dotnet add package MediatR.Extensions.Microsoft.DependencyInjection

We will be using Dapper Micro ORM in this application to do the database operations. I’ll be explaining about the Dapper in a separate article. Install Dapper by running the below command

    dotnet add package Dapper

MediatR mainly uses two interface to implement the Mediator pattern

  • IRequest<T>
  • IRequestHandler<T, U>

More details can be found here

How

I’m going to define a folder structure and naming convention as follows but feel free to use your own conventions.

  • Commands For all commands(CUD Operations), these are POCO classes implements IRequest<T>
  • CommandHandlers All the business logic to execute the commands
  • Queries Same as commands but only for Read operations
  • QueryHandlers All the business logic to execute the queries

I’ve created two DbContexts called ToDoContextRead and ToDoContextWrite both pointing to the same database, but in a Production scenario you can use separate database connection strings for both DbContexts. More on that topic is mentioned here.

ToDoContextRead will look like the below

public class ToDoContextRead{    private readonly string _connectionString;    public ToDoContextRead(IConfiguration configuration)    {        _connectionString = configuration.GetConnectionString("SqlConnectionRead");    }    public IDbConnection CreateConnection()        => new SqlConnection(_connectionString);}

I’ve also created two Repositories for reading and writing to the databases

ToDoRepositoryRead will look like the below:

public class ToDoRepositoryRead : IToDoRepositoryRead{    private readonly ToDoContextRead _context;    public ToDoRepositoryRead(ToDoContextRead context)    {        _context = context;    }    public async Task<ToDo> GetToDoById(Guid id)    {        var query = "SELECT * FROM ToDos where id=@id";        var param = new { id };        using var connection = _context.CreateConnection();        var todo = await connection.QueryFirstOrDefaultAsync<ToDo>(query, param);        return todo;    }    public async Task<IEnumerable<ToDo>> GetToDos()    {        var query = "SELECT * FROM ToDos";        using var connection = _context.CreateConnection();        var todos = await connection.QueryAsync<ToDo>(query);        return todos.ToList();    }}

ToDoRepositoryWrite code is as follows:

public class ToDoRepositoryWrite : IToDoRepositoryWrite{    private readonly ToDoContextWrite _context;    public ToDoRepositoryWrite(ToDoContextWrite context)    {        _context = context;    }    public async Task<int> DeleteToDoById(Guid id)    {        var query = "DELETE FROM ToDos where id=@id";        var param = new { id };        using var connection = _context.CreateConnection();        return await connection.ExecuteAsync(query, param);    }    public async Task<ToDo> GetToDoById(Guid id)    {        var query = "SELECT * FROM ToDos where id=@id";        var param = new { id };        using var connection = _context.CreateConnection();        var todo = await connection.QueryFirstOrDefaultAsync<ToDo>(query, param);        return todo;    }    public async Task<IEnumerable<ToDo>> GetToDos()    {        var query = "SELECT * FROM ToDos";        using var connection = _context.CreateConnection();        var todos = await connection.QueryAsync<ToDo>(query);        return todos.ToList();    }    public async Task<int> SaveToDo(ToDo toDo)    {        var query = @"INSERT INTO ToDos                    (Id, Title, Description, Created, IsCompleted)                    VALUES (@Id, @Title, @Description, @Created, @IsCompleted);";        toDo.Created = DateTime.Now;        using var connection = _context.CreateConnection();        return await connection.ExecuteAsync(query, toDo);    }    public async Task<int> UpdateToDo(ToDo toDo)    {        var query = @"UPDATE ToDos SET                    Title=@Title, Description=@Description, Created=@Created, IsCompleted=@IsCompleted                    WHERE Id=@Id";        toDo.Created = DateTime.Now;        using var connection = _context.CreateConnection();        return await connection.ExecuteAsync(query, toDo);    }}

Commands and Queries

Now the important step is to create the Commands and Queries. Commands and Queries are simple DTOs or POCO classes, but in order to work with the MediatR library, we need to implement an Interface called IRequest<T>. A sample Command is shown below. All other commands and queries can be found in the Github Repo.

public class CreateToDoCommand : IRequest<ToDo>{    public string? Title { get; set; }    public string? Description { get; set; }    public CreateToDoCommand(string? title, string? description)    {        Title = title;        Description = description;    }    public CreateToDoCommand()    {    }}

In the above example, the ToDo class in the IRequest Interface is the return type. For each command or query there should be a Handler defined. Here is the Handler for CreateToDoCommand:

public class CreateToDoCommandHandler : IRequestHandler<CreateToDoCommand, ToDo>{    private readonly IToDoRepositoryWrite _toDoRepositoryWrite;    public CreateToDoCommandHandler(IToDoRepositoryWrite toDoRepositoryWrite)    {        _toDoRepositoryWrite = toDoRepositoryWrite;    }    public async Task<ToDo> Handle(CreateToDoCommand request, CancellationToken cancellationToken)    {        var todo = new ToDo        {            Created = DateTime.Now,            Description = request.Description,            Title = request.Title,            Id = Guid.NewGuid(),            IsCompleted = false        };        var result = await _toDoRepositoryWrite.SaveToDo(todo);        if (result > 0)        {            return todo;        }        else        {            throw new ArgumentException("Unable to save the ToDo");        }    }}

Similar way you can define all your commands, queries and handlers. Once that part is ready then you need to configure the Mediator service in the Program.cs file as follows:

    builder.Services.AddMediatR(typeof(ToDoContextRead).GetTypeInfo().Assembly);

In the above line ToDoContextRead is used just for getting the assembly, and this line will do all the magic to binding the commands and queries to the handlers.

Now you can inject the Mediator into your controller as follows:

private readonly IMediator _mediator;public ToDosController(IMediator mediator){    _mediator = mediator;}

Now you can call any handlers by simply sending the commands as follows

var todos = await _mediator.Send(new GetToDoDetailQuery { Id = id });

Complete code sample can be found at https://github.com/kannan-kiwitech/CqrsMediatorSampleApi.

Happy coding!